Skip to content

Fix Android MediaPicker result recovery#35455

Merged
jfversluis merged 18 commits into
dotnet:net11.0from
AdamEssenmacher:35308
Jun 9, 2026
Merged

Fix Android MediaPicker result recovery#35455
jfversluis merged 18 commits into
dotnet:net11.0from
AdamEssenmacher:35308

Conversation

@AdamEssenmacher

@AdamEssenmacher AdamEssenmacher commented May 15, 2026

Copy link
Copy Markdown

Description of Change

Android can destroy or recreate an app process while another activity is in front. This is especially relevant for camera flows: the Android Activity Result documentation explicitly calls out memory-intensive operations such as camera usage as cases where the launching process/activity may be destroyed, and says result callbacks must be registered unconditionally when the activity is recreated:

https://developer.android.com/training/basics/intents/result

When this happens today, MAUI’s original MediaPicker task is gone. The user can successfully finish the system camera or picker UI, but the app has no reliable way to receive the result. On affected device/app configurations, this effectively makes photo and/or video capture unusable through MediaPicker.

This change adds Android-only recovery support for AndroidX-backed MediaPicker activity results. MAUI now registers the relevant AndroidX activity-result launchers early, persists the active MediaPicker operation before launch, durably records accepted AndroidX callback results, and exposes an opt-in recovery surface so apps can retrieve results after process/activity recreation.

The recovery surface is additive and Android-only:

  • MediaPicker.GetRecoveredMediaPickerResultsAsync()
  • MediaPicker.WaitForRecoveredMediaPickerResultsAsync(CancellationToken)
  • MediaPicker.ClearRecoveredMediaPickerResultAsync(string id)

Recovery covers AndroidX-backed MediaPicker flows across the board:

  • photo capture
  • video capture
  • single photo/video pick
  • multiple photo/video pick

Picker URI handling follows Android’s photo picker guidance around persisted media access:

https://developer.android.com/training/data-storage/shared/photo-picker#persist-media-file-access

Normal live-process behavior is unchanged: existing MediaPicker and IMediaPicker methods still complete normally when the app survives, and no duplicate recovered result is queued.

This also adds Android device-test coverage for the recovery state machine, callback routing, cancellation/wait behavior, duplicate prevention, picker URI materialization, and capture photo processing safety. I also smoke-tested the happy path manually on an API 36 AVD for photo capture, video capture, photo pick, video pick, and multi-photo pick.

Issues Fixed

Fixes #35308

@dotnet-policy-service dotnet-policy-service Bot added the community ✨ Community Contribution label May 15, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Hey there @@AdamEssenmacher! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@github-actions

github-actions Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 35455

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 35455"

@github-actions github-actions Bot added the area-essentials Essentials: Device, Display, Connectivity, Secure Storage, Sensors, App Info label May 15, 2026
@jfversluis

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@kubaflo

kubaflo commented May 19, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/regression-check -p android

@MauiBot MauiBot added s/agent-review-incomplete s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels May 19, 2026
@dotnet dotnet deleted a comment from MauiBot May 19, 2026
@kubaflo

kubaflo commented May 19, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/regression-check -p android

1 similar comment
@kubaflo

kubaflo commented May 19, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/regression-check -p android

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expert Review — 10 findings

See inline comments for details.

Comment thread src/Essentials/src/Platform/PickVisualMediaForResult.android.cs Outdated
Comment thread src/Essentials/src/Platform/PickMultipleVisualMediaForResult.android.cs Outdated
Comment thread src/Essentials/src/Platform/ActivityForResultRequest.android.cs
Comment thread src/Essentials/src/MediaPicker/MediaPickerRecoveryManager.android.cs Outdated
Comment thread src/Essentials/src/MediaPicker/MediaPickerRecoveryManager.android.cs Outdated
Comment thread src/Essentials/src/MediaPicker/MediaPickerRecovery.android.cs

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated review — alternative fix proposed

The expert-reviewer evaluation compared the PR fix against #1 automatically generated candidates and selected try-fix-1 as the strongest fix.

Why: try-fix-1 is the smallest correct fix: it ships the PR's Java pruning (the actual .NET 8->10.0.60 regression root cause) plus the AndroidX ActivityResultLauncher switch, while deleting the 3265-line speculative cross-process recovery layer (state machine, hand-rolled v1-v5 serializer, 3-method public API, 50+ device tests). It resolves all 10 reviewer findings (most as N/A), eliminates the gate's CS0260/CS0115 build errors and the failing MediaPickerRecovery_Tests, and ships zero new public API. The PR (and pr-plus-reviewer) failed the gate; try-fix candidates were predicted-pass (no Helix infra in this session, disclosed in the report).

Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.

Candidate diff (`try-fix-1`)
diff --git a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformMauiAppCompatActivity.java b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformMauiAppCompatActivity.java
index 3b2b9d5551..72dd2e65fa 100644
--- a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformMauiAppCompatActivity.java
+++ b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformMauiAppCompatActivity.java
@@ -1,7 +1,5 @@
 package com.microsoft.maui;
 
-import android.content.Context;
-import android.content.res.Resources;
 import android.content.res.TypedArray;
 
 import androidx.appcompat.app.AppCompatActivity;
@@ -12,11 +10,28 @@ import android.os.Bundle;
  * Class for batching native method calls within the MauiAppCompatActivity implementation
  */
 public class PlatformMauiAppCompatActivity {
+    // These are Android framework / AndroidX saved-instance-state keys. MAUI does not create
+    // the bundles stored under these keys; it only removes or preserves them before AppCompat
+    // restores saved state. AndroidX does not expose public constants for these values.
+    //
+    // ComponentActivity saves pending ActivityResultRegistry state here. Preserving this bundle
+    // lets AndroidX replay pending activity results after activity or process recreation.
+    private static final String ACTIVITY_RESULT_REGISTRY_KEY = "android:support:activity-result";
+
+    // Framework FragmentManager and AndroidX FragmentManager saved-state keys. MAUI removes these
+    // when fragment restore is disabled because restoring old platform fragments can conflict with
+    // MAUI's own navigation/window reconstruction.
+    private static final String ANDROID_FRAGMENTS_KEY = "android:fragments";
+    private static final String SUPPORT_FRAGMENTS_KEY = "android:support:fragments";
+
+    // SavedStateRegistry's top-level bundle key. Older MAUI behavior removed this whole bundle to
+    // suppress fragment restore side effects, but that also discarded ActivityResultRegistry state.
+    private static final String SAVED_STATE_REGISTRY_KEY = "androidx.lifecycle.BundlableSavedStateRegistry.key";
+
     public static void onCreate(AppCompatActivity activity, Bundle savedInstanceState, boolean allowFragmentRestore, int splashAttr, int mauiTheme)
     {
         if (!allowFragmentRestore && savedInstanceState != null) {
-            savedInstanceState.remove("android:support:fragments");
-            savedInstanceState.remove("androidx.lifecycle.BundlableSavedStateRegistry.key");
+            removeFragmentRestoreState(savedInstanceState);
         }
 
         boolean mauiSplashAttrValue = false;
@@ -33,4 +48,30 @@ public class PlatformMauiAppCompatActivity {
             activity.setTheme(mauiTheme);
         }
     }
+
+    private static void removeFragmentRestoreState(Bundle savedInstanceState)
+    {
+        // First remove the direct fragment entries that may be present in the activity state.
+        savedInstanceState.remove(ANDROID_FRAGMENTS_KEY);
+        savedInstanceState.remove(SUPPORT_FRAGMENTS_KEY);
+
+        Bundle savedStateRegistry = savedInstanceState.getBundle(SAVED_STATE_REGISTRY_KEY);
+        if (savedStateRegistry != null) {
+            // The saved-state registry is a shared AndroidX container. Extract the activity-result
+            // entry before removing the container so pending activity results are not lost with the
+            // fragment-related providers.
+            Bundle activityResultRegistryState = savedStateRegistry.getBundle(ACTIVITY_RESULT_REGISTRY_KEY);
+
+            savedInstanceState.remove(SAVED_STATE_REGISTRY_KEY);
+
+            if (activityResultRegistryState != null) {
+                // Keep only the AndroidX ActivityResultRegistry state needed to replay pending
+                // results after activity/process recreation. Other saved-state providers may
+                // contain fragment state that MAUI cannot safely restore.
+                Bundle prunedSavedStateRegistry = new Bundle();
+                prunedSavedStateRegistry.putBundle(ACTIVITY_RESULT_REGISTRY_KEY, activityResultRegistryState);
+                savedInstanceState.putBundle(SAVED_STATE_REGISTRY_KEY, prunedSavedStateRegistry);
+            }
+        }
+    }
 }

diff --git a/CustomAgentLogsTmp/PRState/35455/PRAgent/try-fix-1/csharp-sketch.md b/CustomAgentLogsTmp/PRState/35455/PRAgent/try-fix-1/csharp-sketch.md
new file mode 100644
index 0000000..1111111
--- /dev/null
+++ b/CustomAgentLogsTmp/PRState/35455/PRAgent/try-fix-1/csharp-sketch.md
@@ -0,0 +1,30 @@
+# try-fix-1 C# sketch (companion to Java diff)
+
+The Java hunk above IS the regression fix. The C# delta required to use AndroidX
+launchers (so AndroidX has a registered launcher to replay results into) is:
+
+1. Add three small files (no recovery coupling):
+   * `src/Essentials/src/Platform/CapturePhotoForResult.android.cs` -- thin
+     `ActivityForResultRequest<TakePicture, JavaBoolean>` singleton, registered
+     in `ActivityStateManager` at activity-create time.
+   * `src/Essentials/src/Platform/CaptureVideoForResult.android.cs` -- same
+     pattern for `CaptureVideo` contract.
+   * `src/Essentials/src/Platform/PickVisualMediaForResult.android.cs` /
+     `PickMultipleVisualMediaForResult.android.cs` -- thin AndroidX launcher
+     wrappers that materialize URIs **on Task.Run** (fixes finding F1/F2).
+
+2. Modify `src/Essentials/src/MediaPicker/MediaPicker.android.cs` to use
+   `CapturePhotoForResult.Instance.Launch(outputUri)` /
+   `CaptureVideoForResult.Instance.Launch(outputUri)` instead of the legacy
+   `IntermediateActivity.StartAsync(captureIntent, requestCode, ...)`. The
+   AndroidX launcher MUST be registered before STARTED, which is achieved by
+   adding a one-line registration in `ActivityStateManager.OnActivityCreated`.
+
+3. NO new public API. NO `MediaPickerRecovery.android.cs`. NO
+   `MediaPickerRecoveryManager.android.cs`. NO `MediaPickerRecoveryStore`.
+   NO new entries in `PublicAPI.Unshipped.txt`. NO `MediaPickerRecovery_Tests`.
+
+4. On orphaned-result (process death + AndroidX replay): the registered
+   launcher's no-op `OnActivityResultForOrphanedLaunch` swallows the boolean
+   result; the captured photo file is left on disk in the app's cache and the
+   user re-taps the camera button. Accepted UX trade-off.

@kubaflo

kubaflo commented May 19, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/regression-check -p android

@kubaflo

kubaflo commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

AI code review for net11.0 target

Verdict: Needs discussion

Automated, non-approval review comment (no human approval implied). Reviewed the Android MediaPicker changes and the new recovery subsystem independently before the PR narrative. This is a large change (~4.1k LOC incl. tests) adding a new public API surface, so a careful human pass is warranted.

Observations (no blocking bug found)

  • New public API on Android (MediaPicker.GetRecoveredMediaPickerResultsAsync, WaitForRecoveredMediaPickerResultsAsync, ClearRecoveredMediaPickerResultAsync, DiscardPendingMediaPickerOperationAsync, RecoveredMediaPickerResult, RecoveredMediaPickerResultKind). API naming/shape and the process-recreation recovery contract deserve explicit maintainer/API-review sign-off since it's net-new and Android-only.
  • Concurrency looks deliberate and consistent: MediaPickerRecoveryManager uses a single Lock, a SemaphoreSlim for promotion, and a clear …UnderLock naming convention. I did not spot an obvious deadlock or unguarded shared-state access, but the interplay of BeginOperationWithRecoveryAsync / ClearActiveOperation / waiter completion is intricate enough to merit focused review + the included device tests.
  • File-lifetime safety: ProcessPhotoPreservingSourceAsync intentionally preserves the original capture file until the recovery record is cleared/promoted, and intermediate rotated files are deleted via ShouldDeleteIntermediateFile/TryDeleteFile. Logic reads correct; confirm no temp-file leak when capture is cancelled mid-flight.
  • The useActivityResultCapture = activity is ComponentActivity gate cleanly falls back to the legacy intent path for non-ComponentActivity hosts. Good.
  • Minor: MediaPicker.shared.cs shows the same <remarks> doc line added multiple times — verify it's intentionally attached to each overload and not an accidental duplication on one member.

Confidence / CI

Confidence: medium (breadth + new concurrency/recovery semantics; relied on diff reading, not on-device runs). CI: build/unit legs pass, Build Analysis pending; no PR-attributable failures observed.

@AdamEssenmacher

Copy link
Copy Markdown
Author

Agree with the 'Needs discussion' verdict. That's what I started with on my original comment on the underlying issue

Some high-level discussion points I think are important to frame any follow-up:

  • It is impossible to reliably use the pre-PR state of the MediaPicker / PhotoCapture stuff in Essentials on the Android platform.
  • MAUI's pre-PR handling of recovery state bundles (it just blasts it away) makes it so that developers can't really work around this by using their MediaPicker implementation. The only work-around I'm aware of for this is a fragile hack.
  • I don't think there is a way around the public API change. This is a fundamental Android design thing that simply requires developers to handle this lifecycle event intentionally.
  • Related documentation should really be updated to make it very clear to MAUI developers that using MediaPicker (and other result-from-activity flows) can kill their app process.
  • Other result-from-activity flows can technically suffer from this very same problem (e.g., FilePicker). If public API changes (even purely additive ones like this example) are a non-starter, then fixes for other result-from-activity flows that don't make it into 11 will not be able to make it into MAUI until 12.

@kubaflo

kubaflo commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

AI code review refresh for net11.0 target

Verdict: Needs discussion (unchanged from round 1 — code unchanged, awaiting maintainer/API-review)

Automated, non-approval review comment (no human approval implied). Refresh triggered by @AdamEssenmacher's framing reply. Re-read the diff and re-inspected MediaPickerRecoveryManager, MediaPicker.shared.cs, the Java PlatformMauiAppCompatActivity change, and PublicAPI.Unshipped.txt against this head before responding.

Head reviewed: a38f91985c9d49cb1647e074b0cc19abee28524e (committed 2026-05-24; no new commits since round 1)
Base ref: net11.0
State: OPEN

Reconciliation with author's discussion points

I agree with the framing in the latest comment — these are maintainer-level decisions, not code defects:

  • Pre-PR MediaPicker is unrecoverable on Android process-death configs. Confirmed by reading PlatformMauiAppCompatActivity.onCreate: before this PR, the entire androidx.lifecycle.BundlableSavedStateRegistry.key bundle was removed when allowFragmentRestore=false, which discarded AndroidX ActivityResultRegistry state along with fragment state. The new removeFragmentRestoreState surgically preserves the android:support:activity-result sub-bundle. The Java-side fix is the right shape and the warnIfActivityResultRegistryKeyChanged debug-only reflection guard against AndroidX renaming the constant is a nice safety net.
  • The public API change really does look unavoidable. App code has no way to subscribe to the replayed ActivityResultRegistry callback for a launcher registered inside MAUI; an additive recovery surface owned by MAUI is the natural seam. Worth Microsoft API-review sign-off given Android-only + net-new.
  • Doc update for other result-from-activity flows (FilePicker, etc.): valid concern, out of scope for this PR but worth a follow-up issue.

Code observations from this pass

  • Confirmed correct (retracting round-1 minor Update README.md #5): The repeated <remarks> block in MediaPicker.shared.cs is intentional — it's attached once to each of four distinct members (IMediaPicker.CapturePhotoAsync, IMediaPicker.CaptureVideoAsync, and the two static MediaPicker partial counterparts). Not a duplication bug.
  • Waiter registration race (re-inspected): WaitForRecoveredResultsAsync snapshots RecoveryReconciliationGeneration before the first reconciliation, re-checks it under the lock after registering, and bails out with the empty published results if the generation moved. Combined with RecoveryPromotionSemaphore serializing RecoverOperationIfAvailableUnderSemaphoreAsync and BeginOperationWithRecoveryAsync, the wait→register window looks closed. Good.
  • BeginOperationWithRecoveryAsync retry loop: Bounded at 2 attempts and only re-runs when ShouldPromoteRecreatedOperation is true on attempt 0; the second pass throws via ThrowIfActiveOperationBlocksNewOperation. Forward progress is guaranteed.
  • PublicAPI.Unshipped.txt (net-android only): Adds exactly the documented surface — RecoveredMediaPickerResult, RecoveredMediaPickerResultKind enum (6 members), and four static MediaPicker methods. The unrelated *REMOVED* lines for FilePicker.PickMultipleAsync were already in Unshipped.txt on the base branch and are not touched by this PR.
  • <exception cref="ArgumentException"> on WaitForRecoveredMediaPickerResultsAsync is now consistent with the runtime check that rejects CancellationToken.None — good doc/code alignment.

CI note

  • gh pr checks shows only the lightweight GitHub checks (add-dogfood-comment pass, license/cla pass, Bump global.json skipped, Build Analysis pending since 2026-05-24 — a stale arcade placeholder, not a PR-caused failure).
  • No Azure DevOps statuses are reported on this head (/api/commits/.../status returns statuses:[]). The MAUI required pipelines (maui-pr, maui-pr-devicetests, maui-pr-uitests) do not appear to have been (re-)triggered on a38f919. Before merging, a maintainer should /azp run so maui-pr-devicetests actually exercises the new MediaPickerRecovery_Tests and AndroidXActivityResultRegistryTests device suites on Helix.

Round-1 findings status

All five round-1 observations remain the only items I'd raise; nothing new bug-level surfaced on this re-read. Three of them (API surface needs API-review, file-lifetime safety, useActivityResultCapture fallback) are now also addressed implicitly by the author's framing reply. The remaining open ask is the maintainer/API-review sign-off — this is a discussion, not a code change.

Confidence

  • Code quality / correctness pass: Medium-High. Manual structural reading only; no local build/test on this head.
  • Process-recreation behavior: Medium. Cannot exercise the actual AndroidX process-kill path without a device; device tests in the PR cover the state machine well but maui-pr-devicetests should run.
  • API stability: Needs human/API-review judgement — additive but a permanent surface.

Explicit non-approval disclaimer

This comment is not an approval, does not request changes via the GitHub review system, and is posted only as an inspection/discussion artifact. A human maintainer with API-review authority must make the final call on the new MediaPicker.*RecoveryAsync surface, on whether the additive API is acceptable for net11.0, and on documentation follow-ups for other result-from-activity flows like FilePicker.

@espenrl

espenrl commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

@AdamEssenmacher great work! The recovery event is a super API addition!

I'm an outsider and have solved this exact problem earlier. I want to share some insights which required quite a bit of debugging to figure out. Perhaps others can save time if this is being documented in MAUI.

WebView

What compounds this problem and makes it far worse is any in-app usage of WebView. Why? In low memory conditions one of the top-most app (the app starting the camera) is not the most suceptible to being stopped. This is tracked by the Android lmkd score (low memory killer deamon). The WebView process however has a low lmkd score in these situations. And is one of the first apps being shutdown. How lmkd calculates score may vary by manufactor, but in my experiments it's certainly the case for Samsung devices.

The not so known pain of using WebView

When WebView shuts down it makes sure to shut down any app using the WebView-component for the entire phone. This is by design. It normally happens when the WebView-component (strictly a separate app running as its own process) is being updated. In essence the shutdown of WebView leads to a cascading shutdown of apps.

Any usage at any point in time of WebView will make the app eligible for shutdown

Let's say the MAUI app triggers an OAuth login that happens to use WebView as part of the login process. This is enough. Even though the WebView is shown only once, the tie between the MAUI app and the WebView is forever.

Perhaps this is the nature of the Android WebView-component itself. Perhaps it's something that could be improved in the MAUI code. Perhaps that was the fact on the one phone I used for debugging. I'll leave it for the reader to figure it out.


@kubaflo Hope this can make it into the MAUI documentation.

@espenrl

espenrl commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

This is output from an Android log session when force stopping the WebView. I removed lines not related to the issue, but the order is kept. Look for dotnet.MAUI.app.

adb shell am force-stop com.google.android.webview

Output

onHandleForceStop: [com.google.android.webview]

onReceiveForPackage action = android.intent.action.PACKAGE_RESTARTED, pkgName = com.google.android.webview

handleEvent(0) OnPackageRemoved(packageName=com.google.android.webview) [Module] NavBarStoreImpl

package com.google.android.webview reset

Process 26831 exited due to signal 9 (Killed)

Process 26270 exited due to signal 9 (Killed)

Process 27608 exited due to signal 9 (Killed)

Force stopping com.google.android.webview appid=10295 user=150: from pid 28351

Killing 26324:com.google.android.webview:sandboxed_process0:org.chromium.content.app.SandboxedProcessService0:0/u0a281i322 (adj 0): isolated not needed

Killing 26941:com.google.android.webview:sandboxed_process0:org.chromium.content.app.SandboxedProcessService0:0/u0a448i323 (adj 0): isolated not needed

Killing 26831:dotnet.MAUI.app/u0a448 (adj 0): stop com.google.android.webview due to from pid 28351

Killing 27608:com.google.android.webview:webview_service/u0a295 (adj 905): stop com.google.android.webview due to from pid 28351

Killing 26270:com.google.android.googlequicksearchbox:googleapp/u0a281 (adj 905): stop com.google.android.webview due to from pid 28351

Force stopping com.google.android.webview appid=10295 user=0: from pid 28351

@kubaflo

kubaflo commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

AI code review refresh for net11.0 target

Verdict: Needs discussion (unchanged from rounds 1/3 — no code change; new activity is community discussion, not a defect)

Automated, non-approval review comment (no human approval implied). Refresh triggered by @espenrl's two new comments after the round-3 marker. Re-checked the head, CI, and re-read the changed Android MediaPicker / recovery code before responding.

Head reviewed: a38f91985c9d49cb1647e074b0cc19abee28524e (committed 2026-05-24 — no new commits since round 1)
Base ref: net11.0State: OPEN

What changed since round 3

  • Two informational comments from @espenrl (an external contributor who solved this exact problem before): in-app WebView usage makes a MAUI app eligible for cascading process death, because the Android WebView process has a low lmkd score and its shutdown brings down every app bound to the WebView component (Samsung-confirmed). Includes logcat output showing dotnet.MAUI.app terminated alongside WebView, plus two upstream issuetracker links.
  • This is a testimonial that validates the PR's motivation and a documentation request (@kubaflo Hope this can make it into the MAUI documentation). It surfaces no defect in this PR and requests no code change here.

Assessment of the new discussion

  • The WebView/lmkd cascade is exactly the process-recreation scenario this PR is designed to survive: AndroidX ActivityResultRegistry state is now preserved (instead of being discarded with the fragment bundle) so a pending capture result can be replayed after the activity/process is recreated. So the report strengthens the case for the change rather than challenging it.
  • The documentation ask (WebView as a process-death amplifier; recovery guidance for other result-from-activity flows like FilePicker) is valid but out of scope for this code PR — best tracked as a follow-up docs issue in dotnet/docs-maui rather than blocking this change.

Code re-check (unchanged head)

  • removeFragmentRestoreState still surgically preserves only the android:support:activity-result sub-bundle and drops the rest of the saved-state registry; the debug-only reflection guard (warnIfActivityResultRegistryKeyChanged) and the AndroidXActivityResultRegistryTests device test pin the AndroidX key constant. No regressions from round 3.
  • Public API surface (MediaPicker.*RecoveryAsync, RecoveredMediaPickerResult, RecoveredMediaPickerResultKind) is unchanged and still net-android-only in PublicAPI.Unshipped.txt. The maintainer/API-review sign-off remains the open item.

CI note

  • gh pr checks: only lightweight GitHub checks present — add-dogfood-comment pass, license/cla pass, Bump global.json skipped, Build Analysis pending (stale arcade placeholder, not a PR-caused failure).
  • Commit status API for a38f919 returns state: pending, 0 statuses — the required Azure DevOps pipelines (maui-pr, maui-pr-devicetests, maui-pr-uitests) still don't appear to have run on this head. A maintainer should /azp run so maui-pr-devicetests actually exercises MediaPickerRecovery_Tests and AndroidXActivityResultRegistryTests on Helix before merge. Not claiming ready/LGTM while required device-test coverage hasn't executed.

Confidence

  • Structural correctness: Medium-High (manual reading only; no local build/test on this head).
  • Process-recreation behavior: Medium (state machine + device tests look right, but the real AndroidX kill path needs maui-pr-devicetests).
  • API stability: needs human/API-review judgement (additive but permanent surface).

Explicit non-approval disclaimer

This comment is not an approval and does not request changes via the GitHub review system. It is an inspection/discussion artifact only. A human maintainer with API-review authority owns the final decision on the new recovery API surface and on any documentation follow-ups (including @espenrl's WebView/lmkd guidance).

@jfversluis jfversluis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API surface note.

/// <param name="id">The identifier of the recovered result.</param>
/// <param name="kind">The kind of MediaPicker operation that produced the result.</param>
/// <param name="files">The recovered media files.</param>
public RecoveredMediaPickerResult(string id, RecoveredMediaPickerResultKind kind, IReadOnlyList<FileResult> files)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AdamEssenmacher This otherwise looks good to me. One API-surface question: does RecoveredMediaPickerResult need a public constructor? Since these results are produced by MAUI recovery APIs rather than by app code, making the constructor internal would reduce the permanent public surface while still letting consumers read Id, Kind, and Files. If there’s a consumer construction scenario here, it would be good to call that out; otherwise I’d lean toward making this internal.

@kubaflo

kubaflo commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

AI code review refresh for net11.0 target

Verdict: Needs discussion (carry-over from rounds 1/3/5; a new maintainer API-surface question is now the only open item)

Automated, non-approval review comment (no human approval implied). Refresh triggered by @jfversluis's inline review at 2026-06-08T13:21:32Z on src/Essentials/src/MediaPicker/MediaPickerRecovery.android.cs.

Head reviewed: a38f919 (committed 2026-05-24T22:13:26Z — no new commits since round 5)

What changed since round 5

  • @jfversluis posted a COMMENTED review asking whether RecoveredMediaPickerResult needs a public constructor, and stated "this otherwise looks good to me".
  • No new pushes, no other reviewer activity, no new defects raised.

Independent assessment of the API-surface point

I agree with @jfversluis, and I think the case for making the ctor internal is quite strong:

  • Only producer is MAUI's own recovery pipeline. A repo-wide search for new RecoveredMediaPickerResult returns zero call sites; all construction flows through RecoveredMediaPickerRecord.ToPublicResult() inside MediaPickerRecoveryManager.android.cs (ReadPublicRecoveredResultsUnderLock). There is no documented consumer-construction scenario in the PR description.
  • Tests are unaffected. src/Essentials/test/DeviceTests/Tests/Android/MediaPickerRecovery_Tests.cs constructs RecoveredMediaPickerRecord (internal) and exercises the manager's publish path — it never calls new RecoveredMediaPickerResult(...). Microsoft.Maui.Essentials.DeviceTests already has InternalsVisibleTo, so an internal ctor would still be reachable from tests if ever needed.
  • The type is sealed — no inheritance use case requires public construction.
  • Asymmetric reversibility. internal → public later is non-breaking. public → internal later is a breaking change. With this landing in net11.0 (API window), the conservative direction is to ship internal and widen later if a real consumer need emerges.
  • The PublicAPI.Unshipped.txt entry for the ctor (Microsoft.Maui.Media.RecoveredMediaPickerResult.RecoveredMediaPickerResult(string! id, ... ) -> void) would simply be removed.

This is a one-line visibility change plus a one-line PublicAPI.Unshipped.txt delete. I don't see a defensible reason not to take it before merging into net11.0.

Devil's advocate

  • App-side unit-test mocking? Apps mocking MediaPicker typically wrap it behind an interface and stub the wrapper — they don't construct the result types from MAUI directly. Even when they do, library types are usually wrapped by test doubles too. Not a strong reason for a public ctor.
  • Future deserialization helpers / interop? If an app ever wanted to round-trip a recovered result across processes/persistence, they would still need the internal store format — a public ctor alone wouldn't enable that scenario; it'd require a richer API contract.

Status of prior findings

All ten MauiBot expert-review findings (rounds 1–2) and the rubber-duck pass remain addressed in code at the current head; nothing has regressed.

CI note

gh pr checks at the current head shows only GitHub Actions checks:

  • add-dogfood-comment ✅, license/cla ✅, Bump global.json (skipped), Build Analysis (in-progress — ambient, not a gate failure).

No AzDO maui-pr / maui-pr-devicetests / maui-pr-uitests statuses are reported on this head SHA. That isn't a PR-caused failure — it appears the pipelines simply haven't been triggered on a38f919. A maintainer may want to /azp run to get a full signal on the current head before merging.

Confidence

  • High: API-surface point is well grounded (no construction call sites, sealed type, InternalsVisibleTo in place).
  • Medium: CI completeness — only GHA checks visible on this SHA; no AzDO device/UI-test signal to rely on yet.

This comment is informational only. No approval implied — that decision is for the maintainers.

@kubaflo

kubaflo commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

AI code review refresh for net11.0 target

Verdict: LGTM — the round-12 “Needs discussion” item is resolved. No prior findings remain open.

Automated, non-approval review comment (no human approval implied). Refresh triggered by a new commit at 7ef8810.

Head reviewed: 7ef8810516163c66e0af8f9a11be10b2c88de6ab (committed 2026-06-08T20:32:14Z)
Base: net11.0
State: OPEN, MERGEABLE

What changed since round 12

Single, surgical commit 7ef8810"Make RecoveredMediaPickerResult constructor internal":

  • src/Essentials/src/MediaPicker/MediaPickerRecovery.android.cs: public RecoveredMediaPickerResult(...)internal RecoveredMediaPickerResult(...).
  • src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt: corresponding ctor entry removed.

That is exactly the API-surface narrowing called for in round 12 (and previously by @jfversluis's inline review), and it does not touch any of the recovery, lifecycle, permission, exception-handling, or concurrency code that prior rounds 1–11 scrutinized.

Independent verification of the change

  • No in-repo construction callers. grep -rn "new RecoveredMediaPickerResult" src/ returns zero hits at head. All instances are still produced by RecoveredMediaPickerRecord.ToPublicResult() inside MediaPickerRecoveryManager.android.cs, so the live publish path is unaffected.
  • Type contract preserved for consumers. RecoveredMediaPickerResult remains public sealed; Id, Kind, and Files getters remain public. The public APIs that hand instances back to apps (GetRecoveredMediaPickerResultsAsync, WaitForRecoveredMediaPickerResultsAsync) are unchanged. End users still receive and read recovered results normally — they just can't construct synthetic instances.
  • Tests unaffected. Microsoft.Maui.Essentials.DeviceTests and the other test assemblies are listed in src/Essentials/src/AssemblyInfo/AssemblyInfo.shared.cs InternalsVisibleTo, so the internal ctor stays callable from tests if ever needed; the existing MediaPickerRecovery_Tests.cs constructs RecoveredMediaPickerRecord (internal) and goes through the manager's publish path, so it doesn't depend on a public ctor at all.
  • PublicAPI.Unshipped.txt is correctly updated — only the ctor line is removed; the type entry, getter entries, and enum entries are intact. Format and ordering match analyzer expectations.
  • Visibility direction is reversible-safely. Shipping internal now and widening to public later is non-breaking; the reverse is breaking. Conservative choice for net11.0.

Devil's advocate

  • XML doc summary kept ("Initializes a new instance of the RecoveredMediaPickerResult class.") on the now-internal ctor. Harmless and arguably useful for future internal callers; not worth blocking on.
  • No source/binary compatibility regression for shipped net10 binaries. This type/ctor was introduced in this PR — it is brand new in PublicAPI.Unshipped.txt, never shipped, so narrowing to internal before merge has zero downstream impact.
  • Could a downstream library legitimately need to construct one? Not in any scenario I can identify: result instances exist solely as the output of MAUI's own recovery pipeline, and FileResult ownership/path lifecycle is also managed by that pipeline. Synthesizing one externally would have no useful effect.

Status of prior findings

All MauiBot expert-review items, rubber-duck-pass items, and the round-12 API-surface question are addressed at head. No open carry-over.

CI note

gh pr checks and the GitHub check-runs API at 7ef8810 report only ambient GitHub Actions checks:

Check Status
add-dogfood-comment ✅ success
license/cla ✅ success
Bump global.json skipped
Build Analysis in_progress (ambient, not a gate failure)

No AzDO maui-pr / maui-pr-devicetests / maui-pr-uitests statuses are reported on this head SHA — same pattern as round 12. This is not a PR-caused failure; the pipelines simply have not been triggered on 7ef8810. Because this last commit is purely an accessibility-modifier + PublicAPI.Unshipped.txt change (no executable code paths touched), the risk of a regression vs. the previously-tested a38f919 is minimal. That said, a maintainer should still /azp run to land full pre-merge signal before clicking merge, since net11.0 deserves a green AzDO run on the actual head.

Confidence

  • High: the round-12 ask is now resolved correctly and minimally; no functional code is touched.
  • Medium: CI completeness — only GHA checks visible on this SHA; no AzDO device/UI-test run on 7ef8810 to lean on, although the change shouldn't affect runtime behavior.

This comment is informational only. No approval implied — that decision is for the maintainers.

@jfversluis jfversluis merged commit d3684fa into dotnet:net11.0 Jun 9, 2026
3 of 4 checks passed
@AdamEssenmacher AdamEssenmacher deleted the 35308 branch June 9, 2026 18:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-essentials Essentials: Device, Display, Connectivity, Secure Storage, Sensors, App Info area-essentials-mediapicker community ✨ Community Contribution platform/android s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) t/bug Something isn't working t/enhancement ☀️ New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Capturing a photo using MediaPicker.CapturePhotoAsync crashes if the user takes too long to take the picture.

6 participants